<template>
    <div :class="cn('relative h-60 w-full', props.containerClass)">
        <Motion ref="containerRef" as="div" :initial="{ opacity: 0 }" :animate="{ opacity: 1 }"
            class="absolute inset-0 z-0 flex size-full items-center justify-center bg-transparent">
            <canvas ref="canvasRef" class="h-60 w-full"></canvas>
        </Motion>

        <div :class="cn('relative h-full z-10 text-2xl text-white flex justify-center items-center', props.class,)" >
            <slot />
        </div>
    </div>
</template>

<script setup lang="ts">
import { createNoise3D } from "simplex-noise";
import { onMounted, onUnmounted, ref, shallowRef } from "vue";
import { templateRef, useDebounceFn } from "@vueuse/core";
import { cn } from "@/lib/utils";


const TAU = 2 * Math.PI;
const BASE_TTL = 50;
const RANGE_TTL = 150;
const PARTICLE_PROP_COUNT = 9;
const RANGE_HUE = 100;
const NOISE_STEPS = 3;
const X_OFF = 0.00125;
const Y_OFF = 0.00125;
const Z_OFF = 0.0005;

interface VortexProps {
    class?: string;
    containerClass?: string;
    particleCount?: number;
    rangeY?: number;
    baseHue?: number;
    baseSpeed?: number;
    rangeSpeed?: number;
    baseRadius?: number;
    rangeRadius?: number;
    backgroundColor?: string;
}

const props = withDefaults(defineProps<VortexProps>(), {
    particleCount: 700,
    rangeY: 100,
    baseSpeed: 0.0,
    rangeSpeed: 1.5,
    baseRadius: 1,
    rangeRadius: 2,
    baseHue: 220,
    backgroundColor: "#000000",
});

const tick = ref<number>(0);
const animationFrame = ref<number | null>(null);
const particleProps = shallowRef<Float32Array | null>(null);
const center = ref<[number, number]>([0, 0]);
const ctx = shallowRef<CanvasRenderingContext2D | null>(null);

const canvasRef = templateRef<HTMLCanvasElement | null>("canvasRef");
const containerRef = templateRef<HTMLElement | null>("containerRef");

const particleCache = {
    x: 0,
    y: 0,
    vx: 0,
    vy: 0,
    life: 0,
    ttl: 0,
    speed: 0,
    radius: 0,
    hue: 0,
};

const noise3D = createNoise3D();

function rand(n: number) {
    return n * Math.random();
}
function randRange(n: number): number {
    return n - rand(2 * n);
}
function fadeInOut(t: number, m: number): number {
    const hm = 0.5 * m;
    return Math.abs(((t + hm) % m) - hm) / hm;
}
function lerp(n1: number, n2: number, speed: number): number {
    return (1 - speed) * n1 + speed * n2;
}

function initParticle(i: number) {
    if (!particleProps.value || !canvasRef.value) return;

    const canvas = canvasRef.value;
    particleCache.x = rand(canvas.width);
    particleCache.y = center.value[1] + randRange(props.rangeY);
    particleCache.vx = 0;
    particleCache.vy = 0;
    particleCache.life = 0;
    particleCache.ttl = BASE_TTL + rand(RANGE_TTL);
    particleCache.speed = props.baseSpeed + rand(props.rangeSpeed);
    particleCache.radius = props.baseRadius + rand(props.rangeRadius);
    particleCache.hue = props.baseHue + rand(RANGE_HUE);

    particleProps.value.set(
        [
            particleCache.x,
            particleCache.y,
            particleCache.vx,
            particleCache.vy,
            particleCache.life,
            particleCache.ttl,
            particleCache.speed,
            particleCache.radius,
            particleCache.hue,
        ],
        i,
    );
}

function updateParticle(i: number) {
    if (!particleProps.value || !canvasRef.value || !ctx.value) return;

    const canvas = canvasRef.value;
    const props = particleProps.value;
    const context = ctx.value;

    particleCache.x = props[i];
    particleCache.y = props[i + 1];
    particleCache.vx = props[i + 2];
    particleCache.vy = props[i + 3];
    particleCache.life = props[i + 4];
    particleCache.ttl = props[i + 5];
    particleCache.speed = props[i + 6];
    particleCache.radius = props[i + 7];
    particleCache.hue = props[i + 8];

    const n =
        noise3D(particleCache.x * X_OFF, particleCache.y * Y_OFF, tick.value * Z_OFF) *
        NOISE_STEPS *
        TAU;

    const nextVx = lerp(particleCache.vx, Math.cos(n), 0.5);
    const nextVy = lerp(particleCache.vy, Math.sin(n), 0.5);
    const nextX = particleCache.x + nextVx * particleCache.speed;
    const nextY = particleCache.y + nextVy * particleCache.speed;

    context.save();
    context.lineCap = "round";
    context.lineWidth = particleCache.radius;
    context.strokeStyle = `hsla(${particleCache.hue},100%,60%,${fadeInOut(
        particleCache.life,
        particleCache.ttl,
    )})`;
    context.beginPath();
    context.moveTo(particleCache.x, particleCache.y);
    context.lineTo(nextX, nextY);
    context.stroke();
    context.restore();

    props[i] = nextX;
    props[i + 1] = nextY;
    props[i + 2] = nextVx;
    props[i + 3] = nextVy;
    props[i + 4] = particleCache.life + 1;

    if (
        nextX > canvas.width ||
        nextX < 0 ||
        nextY > canvas.height ||
        nextY < 0 ||
        particleCache.life > particleCache.ttl
    ) {
        initParticle(i);
    }
}

function draw() {
    if (!canvasRef.value || !ctx.value || !particleProps.value) return;

    const canvas = canvasRef.value;
    const context = ctx.value;

    tick.value++;

    context.fillStyle = props.backgroundColor;
    context.fillRect(0, 0, canvas.width, canvas.height);

    for (let i = 0; i < particleProps.value.length; i += PARTICLE_PROP_COUNT) {
        updateParticle(i);
    }

    context.save();
    context.filter = "blur(8px) brightness(200%)";
    context.globalCompositeOperation = "lighter";
    context.drawImage(canvas, 0, 0);
    context.restore();

    context.save();
    context.filter = "blur(4px) brightness(200%)";
    context.globalCompositeOperation = "lighter";
    context.drawImage(canvas, 0, 0);
    context.restore();

    animationFrame.value = requestAnimationFrame(draw);
}

const handleResize = useDebounceFn(() => {
    if (!canvasRef.value || !containerRef.value) return;

    const canvas = canvasRef.value;
    const container = containerRef.value;
    const rect = container.getBoundingClientRect();
    
    canvas.width = rect.width;
    canvas.height = rect.height;
    center.value = [0.5 * canvas.width, 0.5 * canvas.height];
}, 150);

onMounted(() => {
    const canvas = canvasRef.value;
    if (!canvas) return;

    ctx.value = canvas.getContext("2d");
    if (!ctx.value) return;

    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    center.value = [0.5 * canvas.width, 0.5 * canvas.height];

    const particlePropsLength = props.particleCount * PARTICLE_PROP_COUNT;
    particleProps.value = new Float32Array(particlePropsLength);

    for (let i = 0; i < particlePropsLength; i += PARTICLE_PROP_COUNT) {
        initParticle(i);
    }

    draw();
    window.addEventListener("resize", handleResize);
});

onUnmounted(() => {
    if (animationFrame.value) {
        cancelAnimationFrame(animationFrame.value);
    }
    window.removeEventListener("resize", handleResize);

    ctx.value = null;
    particleProps.value = null;
});
</script>